Skip to content

refactor(alerts): unify on the v2 alert system - #293

Merged
Makisuo merged 4 commits into
mainfrom
agent/unify-alert-system
Jul 31, 2026
Merged

refactor(alerts): unify on the v2 alert system#293
Makisuo merged 4 commits into
mainfrom
agent/unify-alert-system

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • remove the v1 alerts HTTP surface and make v2 the only control plane, including canonical delivery history
  • route custom metric alerts through the shared query-builder/query-engine path and remove direct metric schema, storage, and runtime paths
  • remove legacy Slack/Hazel webhook destinations and UI compatibility fallbacks
  • add migration 0026_windy_bromley to convert metric rules and delete retired destination data

Why

Alerts had parallel HTTP, evaluation, storage, provider, and UI paths. This hard cutover leaves one Postgres-backed alert service and one shared query-engine evaluation path.

User and operator impact

  • Existing direct metric rules are converted to builder_query drafts.
  • Legacy slack and hazel destinations, plus their delivery history, are deleted.
  • Rules left without destinations are disabled.
  • Existing integrations must use slack-bot or hazel-oauth.
  • /api/alerts is removed; clients must use /v2/alerts/*.

Deployment warning

This is a destructive, forward-only hard cutover. Production rollout should:

  1. take a restorable database backup
  2. deploy the application code
  3. immediately apply 0026_windy_bromley through the direct PlanetScale connection with runtime grants
  4. smoke-test /v2/alerts/{rules,destinations,incidents,deliveries} and the scheduler

Reverting after the migration requires a database restore or a forward fix.

Validation

  • bun typecheck — 33 packages passed
  • API alert suite — 110 tests passed
  • focused web alert suites, including final chart-prefill checks — passed
  • domain contracts — 64 tests passed
  • query engine — 70 tests passed
  • database migrations — 3 tests passed
  • Alchemy provider — 17 tests passed
  • React Doctor — no changed-file diagnostics
  • git diff --check — clean

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Ingest Rust Test + Benchmark Results

Commit: 314120ed0cf168a5c47763059d2ae943ad547566

Load Benchmark — tinybird mode, median of 3 run(s) vs main

Metric main (median) PR (median) Delta
Requests/sec 11086.22 10449.08 -5.7% worse
Rows/sec 110862.19 104490.84 -5.7% worse
p50 latency 5.49 ms 5.86 ms +6.7% worse
p95 latency 10.81 ms 11.69 ms +8.1% worse
p99 latency 12.98 ms 14.04 ms +8.1% worse
Export catch-up 0.027 s 0.027 s -0.2% better
Max RSS 15.59 MiB 16.45 MiB +5.6% worse
Failures 0 0 same

Same code path on both sides (same LOAD_TEST_INGEST_MODE), so the delta column is meaningful. Numbers come from ubuntu-latest, which is noisy — treat single-digit-percent deltas as noise.

PR load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.191404342,
    "export_catchup_seconds": 0.025095261,
    "request_rps": 10449.083751715518,
    "row_rps": 104490.83751715517,
    "p50_ms": 5.861,
    "p95_ms": 10.876,
    "p99_ms": 12.875,
    "max_rss_mb": 18.12109375,
    "max_cpu_percent": 20.0,
    "avg_cpu_percent": 20.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.185584241,
    "export_catchup_seconds": 0.026502793,
    "request_rps": 10776.77710792265,
    "row_rps": 107767.77107922648,
    "p50_ms": 5.833,
    "p95_ms": 11.694,
    "p99_ms": 14.08,
    "max_rss_mb": 15.64453125,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 12,
    "duration_seconds": 0.20961849,
    "export_catchup_seconds": 0.02662882,
    "request_rps": 9541.14305469904,
    "row_rps": 95411.43054699039,
    "p50_ms": 6.442,
    "p95_ms": 12.525,
    "p99_ms": 14.037,
    "max_rss_mb": 16.453125,
    "max_cpu_percent": 25.0,
    "avg_cpu_percent": 25.0
  }
]
main load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.180404154,
    "export_catchup_seconds": 0.026648533,
    "request_rps": 11086.219223089507,
    "row_rps": 110862.19223089507,
    "p50_ms": 5.57,
    "p95_ms": 10.815,
    "p99_ms": 12.984,
    "max_rss_mb": 15.953125,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 10,
    "duration_seconds": 0.165657708,
    "export_catchup_seconds": 0.026567801,
    "request_rps": 12073.087477462866,
    "row_rps": 120730.87477462867,
    "p50_ms": 4.978,
    "p95_ms": 9.875,
    "p99_ms": 11.769,
    "max_rss_mb": 15.5859375,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 11,
    "duration_seconds": 0.18089209,
    "export_catchup_seconds": 0.026307864,
    "request_rps": 11056.315397760067,
    "row_rps": 110563.15397760067,
    "p50_ms": 5.491,
    "p95_ms": 11.345,
    "p99_ms": 15.98,
    "max_rss_mb": 15.34765625,
    "max_cpu_percent": 0.0,
    "avg_cpu_percent": 0.0
  }
]

WAL-acked microbench (cargo bench --bench ingest_bench)

   Compiling maple-ingest v0.1.0 (/home/runner/_work/maple/maple/apps/ingest)
    Finished `bench` profile [optimized] target(s) in 17.71s
     Running benches/ingest_bench.rs (target/release/deps/ingest_bench-f6ec3881b8bd23cb)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench:      136504 ns/iter (+/- 18339)
test ingest_accept/traces_10_spans_wal_ack ... bench:      168879 ns/iter (+/- 30132)

cargo test


test result: ok. 66 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.73s

     Running unittests src/bin/load_test.rs (target/debug/deps/load_test-3a24fd5dee8f57f0)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/maple_ingest-b89fa52019d5d48f)

running 60 tests
test autumn::tests::flat_hardcap_depleted_blocks ... ok
test autumn::tests::allowed_only_no_balance_field ... ok
test autumn::tests::flat_hardcap_with_remaining_allows ... ok
test autumn::tests::flat_overage_allows ... ok
test autumn::tests::flat_sub_one_gb_remaining_still_allows ... ok
test autumn::tests::nested_balance_object_depleted_blocks ... ok
test autumn::tests::flat_unlimited_allows ... ok
test autumn::tests::nested_balance_object_with_remaining_allows ... ok
test autumn::tests::nested_overage_allows ... ok
test autumn::tests::unrecognized_shape_returns_none ... ok
test autumn::tests::null_balance_no_subscription_blocks ... ok
test tests::a_browser_sessions_cap_blocks_replay_without_touching_other_signals ... ok
test tests::api_error_from_pipeline_maps_variants_to_status ... ok
test tests::api_error_kind_maps_status_to_stable_label ... ok
test tests::caller_fault_rejections_do_not_mark_the_span_error ... ok
test tests::clickhouse_destination_is_terminal_in_dual_mode ... ok
test tests::clickhouse_destination_uses_native_pipeline_even_in_forward_mode ... ok
test tests::clickhouse_target_resolver_decrypts_current_schema_password ... ok
test tests::clickhouse_target_resolver_rejects_password_over_http ... ok
test tests::clickhouse_target_resolver_requires_current_schema ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::cloudflare_log_record_maps_body_severity_and_attributes ... ok
test tests::cloudflare_timestamps_support_rfc3339_unix_and_unix_nano ... ok
test tests::cloudflare_validation_payload_is_detected ... ok
test tests::data_loss_labels_match_the_kinds_the_gateway_emits ... ok
test tests::data_loss_rejections_mark_the_span_error_even_at_4xx ... ok
test tests::decrypt_aes256_gcm_matches_node_crypto_fixture ... ok
test tests::empty_export_request_is_accepted ... ok
test tests::enrichment_applies_to_a_payload_with_no_resource ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::hash_is_deterministic ... ok
test tests::lenient_log_payload_is_accepted ... ok
test tests::malformed_payload_is_rejected_with_the_parser_reason ... ok
test autumn::tests::fails_open_on_transport_error ... ok
test autumn::tests::fail_open_decisions_are_not_cached ... ok
test tests::numeric_nanos_no_longer_silently_empty_a_metric ... ok
test tests::pipeline_error_fault_split_matches_http_status ... ok
test tests::rejection_reason_is_bounded ... ok
test tests::replay_budget_disabled_when_limit_is_zero ... ok
test tests::replay_budget_scopes_totals_per_session_and_org ... ok
test tests::replay_budget_truncates_session_at_its_ceiling ... ok
test tests::resolve_connector_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_carries_spend_pause_state_from_the_key_lookup ... ok
test tests::resolve_ingest_key_keeps_stale_schema_on_managed_native_path ... ok
test tests::native_request_emits_a_span_per_pipeline_stage ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::replay_metadata_is_rejected_and_unmetered_when_sessions_are_capped ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::server_faults_stay_error ... ok
test tests::spec_compliant_payloads_still_decode ... ok
test tests::spend_blocks_signal_pauses_the_whole_org_or_just_the_capped_signal ... ok
test tests::tinybird_destination_keeps_forward_mode_on_forward_path ... ok
test tests::resolve_ingest_key_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_serves_last_known_routing_when_refresh_fails ... ok
test tests::forward_mode_switches_ready_org_to_clickhouse_without_forwarding_again ... ok
test autumn::tests::usage_after_a_successful_flush_gets_a_fresh_key ... ok
test autumn::tests::a_failed_flush_retries_under_the_same_idempotency_key ... ok

test result: ok. 60 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.03s

   Doc-tests maple_ingest

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🍁 Maple PR preview

Note

Preview resources were removed when this pull request closed.

Final commit 0ea774b · View workflow run

@Makisuo
Makisuo marked this pull request as ready for review July 31, 2026 11:00
@pullfrog

pullfrog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog

pullfrog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog

pullfrog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

Migrations created tables with owner-only privileges, so every new or
rebuilt table had to be healed by a post-migrate grant pass keyed to
MAPLE_PG_RUNTIME_ROLE. Forgetting that env var on main is what caused the
2026-07-29 ingest outage, and the hard failure guarding it made the prod
migration unrunnable without hunting down a role name by hand.

Pin the migration session to `postgres` (ALTER ROLE ... SET role, so it
applies to the connections drizzle-kit opens itself) and install default
privileges granting PUBLIC before migrating. New tables are then born with
correct grants and the sweep is only a backfill for pre-existing objects.

PUBLIC rather than a named role is what actually matters: three of prod's
four pscale_api_* login roles are members of `postgres` with rolinherit and
read a postgres-owned table through inheritance, but the ingest gateway
connects via PSBouncer as pscale_api_rg068pnctlxw, which is not a member and
reads only through PUBLIC. That asymmetry is why the outage broke the
gateway alone while the API and alerting workers kept serving.

- replace grant-runtime-role.ts with ensure-privileges.ts (db:ensure-privileges)
- drop the MAPLE_PG_RUNTIME_ROLE guard and env var entirely
- stg/pr-preview workflows run the step BEFORE migrate; ordering is
  load-bearing since default privileges only affect objects created after

Verified on pr-152: a table created after the change goes from relacl=null
to PUBLIC arwd with no grant pass and no configuration.
@pullfrog

pullfrog Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Your Pullfrog Router balance is exhausted.

You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.

Top up balance → · Enable auto-reload →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@Makisuo
Makisuo merged commit 50ef118 into main Jul 31, 2026
20 of 21 checks passed
@Makisuo
Makisuo deleted the agent/unify-alert-system branch July 31, 2026 16:05
@Makisuo
Makisuo deployed to pr-preview July 31, 2026 16:05 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant